feat(citations): add Org 9.5 citation support with BibTeX source, completions, and go-to-source#1113
feat(citations): add Org 9.5 citation support with BibTeX source, completions, and go-to-source#1113NickHu wants to merge 5 commits into
Conversation
|
I created a new release of the TS parser (https://github.com/nvim-orgmode/tree-sitter-org/releases/tag/2.0.3). |
|
@kristijanhusak rebased onto master and tests passing |
There was a problem hiding this comment.
Pull request overview
Adds Org mode 9.5 citation support to the Neovim orgmode plugin, including BibTeX-backed key discovery, omnifunc completion, syntax highlighting, and “go to bibliography entry” behavior via the existing org_open_at_point mapping.
Changes:
- Introduces a new
OrgCitationssubsystem with a built-in BibTeX source and extensible custom source API. - Adds citation key omnifunc completion plus
#+bibliographydirective completion. - Adds citation highlighting, documentation, tests/fixtures, and bumps the required tree-sitter-org grammar version.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
lua/orgmode/org/citations/init.lua |
New citations manager (source registration, item aggregation, follow, at-cursor resolution). |
lua/orgmode/org/citations/bibtex.lua |
Built-in BibTeX citation source (path discovery, parsing, mtime caching, follow-to-entry). |
lua/orgmode/org/citations/_meta.lua |
Type metadata for citation items/sources. |
lua/orgmode/org/autocompletion/sources/citations.lua |
Omnifunc completion source for citation keys inside [cite...]. |
lua/orgmode/org/autocompletion/sources/directives.lua |
Adds #+bibliography to directive completions. |
lua/orgmode/org/autocompletion/init.lua |
Wires citations into completion and registers the new completion source. |
lua/orgmode/org/mappings.lua |
Extends org_open_at_point to follow citation keys under cursor. |
lua/orgmode/init.lua |
Instantiates OrgCitations and passes it into mappings/completion. |
lua/orgmode/config/defaults.lua |
Adds default citations config keys (sources, org_cite_global_bibliography). |
queries/org/highlights.scm |
Adds highlight captures for citation nodes. |
lua/orgmode/colors/highlights.lua |
Maps new citation captures to existing highlight groups. |
lua/orgmode/utils/treesitter/install.lua |
Bumps required tree-sitter-org grammar version. |
docs/configuration.org |
Documents citation syntax, bibliography configuration, and custom sources (incl. Zotero example). |
tests/plenary/org/citations/citations_spec.lua |
Tests citations source registration, aggregation, and follow dispatch. |
tests/plenary/org/citations/bibtex_spec.lua |
Tests BibTeX parsing, bibliography discovery, follow behavior, and completion regex. |
tests/plenary/org/autocompletion_spec.lua |
Updates directive completion expectations for #+bibliography. |
tests/plenary/fixtures/citations/refs.bib |
BibTeX fixture used by tests. |
tests/plenary/fixtures/citations/extra.bib |
Additional BibTeX fixture used by tests. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| if raw:sub(1, 1) ~= '/' then | ||
| local base = base_dir or vim.fn.getcwd() | ||
| return vim.fn.fnamemodify(base .. '/' .. raw, ':p') | ||
| end |
| local file = self.files:load_file_sync(current_filename) | ||
| if file then | ||
| local file_dir = vim.fn.fnamemodify(file.filename, ':p:h') | ||
| local directives = file:_get_directive('bibliography', true) | ||
| if directives then | ||
| if type(directives) == 'string' then | ||
| directives = { directives } | ||
| end | ||
| for _, raw in ipairs(directives) do | ||
| add(raw, file_dir) | ||
| end |
| --- Build a simple in-memory citation source for testing. | ||
| ---@param name string | ||
| ---@param items OrgCitationItem[] | ||
| ---@param follow_handler? fun(key: string): boolean |
| for i, source in ipairs(config.citations.sources) do | ||
| if type(source.get_name) == 'function' then | ||
| self:add_source(source) | ||
| else | ||
| vim.notify(('Citation source at index %d must have a get_name method'):format(i), vim.log.levels.ERROR) |
| local function parse_file(path) | ||
| local stat = vim.uv.fs_stat(path) | ||
| if not stat then | ||
| return {} | ||
| end | ||
| local mtime_sec = stat.mtime.sec | ||
| local cached = _cache[path] | ||
| if cached and cached.mtime_sec == mtime_sec then | ||
| return cached.items | ||
| end | ||
| local lines = vim.fn.readfile(path) | ||
| local items = parse_bibtex(table.concat(lines, '\n')) | ||
| _cache[path] = { mtime_sec = mtime_sec, items = items } | ||
| return items |
kristijanhusak
left a comment
There was a problem hiding this comment.
Sorry for the late review. Overall it seems to be working fine. I'd just like to refactor few things before we merge it in.
| return self:_jump_to_footnote(footnote) | ||
| end | ||
|
|
||
| if self.citations then |
There was a problem hiding this comment.
Why do we need this check?
| }, | ||
| citations = { | ||
| sources = {}, | ||
| org_cite_global_bibliography = {}, |
There was a problem hiding this comment.
Let's move this to the top level config as other things to make it consistent. Sources configuration can stay like this.
| ---@param raw string | ||
| ---@param base_dir? string | ||
| ---@return string | ||
| local function resolve_path(raw, base_dir) |
There was a problem hiding this comment.
We can use get_real_path from utils.fs.lua instead of this function.
| @@ -0,0 +1,155 @@ | |||
| local config = require('orgmode.config') | |||
There was a problem hiding this comment.
Since we also support custom sources with this PR, we can create an org/citations/sources folder and put this there. I see that Emacs orgmode also supports json format so we can also add that as a built in.
| local file = self.files:load_file_sync(current_filename) | ||
| if file then | ||
| local file_dir = vim.fn.fnamemodify(file.filename, ':p:h') | ||
| local directives = file:_get_directive('bibliography', true) |
There was a problem hiding this comment.
Let's not use private methods outside of the file. Add a public method file:get_bibliography and do this inside.
| local directives = file:_get_directive('bibliography', true) | |
| local directives = file:get_bibliography() |
| if cached and cached.mtime_sec == mtime_sec then | ||
| return cached.items | ||
| end | ||
| local lines = vim.fn.readfile(path) |
There was a problem hiding this comment.
We should use async file reading whenever possible. Look into utils.readfile. This would require refactoring few things to be async, but shouldn't be big of a problem.
Also, what Copilot mentioned below, try using nsec when available. When it is not available, nsec is 0, so you need to fallback to sec. You can check how it's done in files/file.lua.
| end | ||
| end | ||
|
|
||
| if self.files then |
There was a problem hiding this comment.
We can default self.files to empty table in the new() so there's no need to do these kind of checks.
Summary
This PR adds support for Citations, introduced by orgmode 9.5.
Related Issues
Depends on nvim-orgmode/tree-sitter-org#7 (this is why the tests fail)
Closes #718
Changes
lua/orgmode/org/citations/init.lua—OrgCitationsclass: registers sources, aggregatesget_items(), dispatchesfollow(key), and resolves the citation key at cursor via tree-sittercitation_referencenodelua/orgmode/org/citations/bibtex.lua— built-inOrgCitationBibtexsource; parses.bibfiles (mtime-cached), resolves paths fromcitations.org_cite_global_bibliographyconfig and file-local#+bibliography:directives, implementsfollow(key)to open the file at the entry linelua/orgmode/org/autocompletion/sources/citations.lua—omnifunccompletion source; matches[cite:@/[cite/style:@prefix and returns keys from all registered sourceslua/orgmode/config/defaults.lua— addscitations.org_cite_global_bibliographyandcitations.sourcesdefaultslua/orgmode/colors/highlighter/markup/citation.lua+queries/org/markup.scm— syntax highlighting for citation nodeslua/orgmode/org/mappings.lua—org_open_at_pointnow delegates tocitations:follow()when on a citation keydocs/configuration.org— newCitationssection covering syntax, bibliography config, custom source API, and Zotero Local API exampleChecklist
I confirm that I have:
Conventional Commits
specification (e.g.,
feat: add new feature,fix: correct bug,docs: update documentation).make test.Warning
The bulk of this PR is AI-generated, although I have combed over it extensively and cleaned it up by hand and fixed things.
It is basically exactly the changes you would expect to see from the PR title, except for the following design considerations:
lua/orgmode/org/citations/_meta.luaadds two fieldslabelanddescriptiontoOrgCitationItem, which currently aren't used anywhere; the hope is that these items can be utilised in the completion popup, but it seems like support for ancillary information in completion items is not fleshed out in nvim-orgmode yet as a whole. The intended use-case is so that the completion call/popup can show or react to something likeDoe, John 2026 Some Article(label) while still completing the key@doe2026, and LSP hover can display extra information (like an article abstract) from thedescriptionfield.I have tested it extensively, and I think it is ready for upstream review, but I am marking it as draft for the above reasons.